Keychain

Keychain provides secure access to the system keychain for storing sensitive and persistent data inside the Scripting environment. It is designed for:

  • Authentication tokens
  • Login credentials
  • License and subscription states
  • Encryption keys
  • Private user data

All data is protected using the system-level Keychain security mechanism.


1. Per-Script Keychain Scope

In Scripting, Keychain uses a per-script isolation model.

1.1 Scope Rules

  • Each script has its own independent Keychain scope

  • A script can only access the Keychain data it has written

  • Different scripts:

    • Cannot read each other’s Keychain data
    • Cannot overwrite each other’s keys
    • Even if the same key name is used
    • Even if synchronizable: true is enabled
  • Each script is treated as an independent security sandbox


1.2 Security Implications

This design ensures that:

  • No script can steal credentials from another script
  • Subscription, login state, and authorization data are fully isolated
  • Malicious scripts cannot access private user data stored by other scripts
  • The security boundary is stricter than the system-level app Keychain alone

1.3 Script Removal Behavior

  • When a script is deleted:

    • All Keychain data under that script’s scope is automatically removed
  • Other scripts’ Keychain data is not affected


2. Namespace

namespace Keychain

3. Supported Data Types

Keychain supports three data types:

TypeWriteRead
Stringsetget
BooleansetBoolgetBool
Binary DatasetDatagetData

4. KeychainAccessibility

type KeychainAccessibility =
  | 'passcode'
  | 'unlocked'
  | 'unlocked_this_device'
  | 'first_unlock'
  | 'first_unlock_this_device'
ValueDescription
passcodeAccessible only when a device passcode is set; does not migrate to a new device
unlockedAccessible only while the device is unlocked
unlocked_this_deviceAccessible only on the current device; does not migrate
first_unlockAccessible after the first unlock following a restart
first_unlock_this_deviceSame as first_unlock, but does not migrate

The default depends on whether iCloud synchronization is enabled:

synchronizable: false // accessibility: "first_unlock_this_device"
synchronizable: true  // accessibility: "first_unlock"

When synchronizable: true is used, passcode, unlocked_this_device, and first_unlock_this_device are not allowed because they have ThisDeviceOnly semantics and cannot synchronize to other devices. Writes with these combinations fail and return false.


5. iCloud Synchronization (synchronizable)

synchronizable?: boolean
ValueDescription
trueSynchronizes across devices using the same Apple ID
falseStored only on the local device

Default:

synchronizable: false

When enabled, Keychain items use the migratable default accessibility policy first_unlock. If you customize accessibility, use a synchronizable policy such as unlocked or first_unlock.

Even when enabled, synchronization is still restricted to the current script scope.


6. Writing Data

6.1 Store a String

Keychain.set(key: string, value: string, options?): boolean
Keychain.set("token", "abcdef")
Keychain.set("token", "abcdef", {
  accessibility: "first_unlock",
  synchronizable: true
})

6.2 Store a Boolean

Keychain.setBool(key: string, value: boolean, options?): boolean
Keychain.setBool("is_login", true)

6.3 Store Binary Data

Keychain.setData(key: string, value: Data, options?): boolean
Keychain.setData("avatar", imageData)

6.4 Overwrite Rules

  • Existing values are automatically overwritten
  • true is returned on success
  • false is returned on failure

7. Reading Data

7.1 Read a String

Keychain.get(key: string, options?): string | null

7.2 Read a Boolean

Keychain.getBool(key: string, options?): boolean | null

7.3 Read Binary Data

Keychain.getData(key: string, options?): Data | null

8. Removing Data

Keychain.remove(key: string, options?): boolean
  • If the key exists, it is deleted and returns true
  • If the key does not exist, it still safely returns true

9. Checking for Key Existence

Keychain.contains(key: string, options?): boolean

10. Listing All Keys

Keychain.keys(options?): string[]

11. Clearing the Keychain

Keychain.clear(options?): boolean

Behavior:

  • Only clears data within the current script scope
  • Does not affect other scripts
  • Does not affect the app’s own Keychain data or other apps

12. synchronizable Read/Write Consistency Rules

If a key is written with:

synchronizable: true

Then all subsequent operations must use the same flag:

Keychain.set("token", "abc", { synchronizable: true })

Keychain.get("token") // Cannot read
Keychain.get("token", { synchronizable: true }) // Can read

13. Security Recommendations

Suitable Data

  • Authentication tokens
  • Subscription and license states
  • User identifiers
  • Encryption keys
  • Large binary files
  • High-frequency cache data
  • Public configuration values

14. Typical Usage Examples

// Write
Keychain.set("token", "abcdef")
Keychain.setBool("is_login", true)
Keychain.setData("avatar", avatarData)

// Read
const token = Keychain.get("token")
const isLogin = Keychain.getBool("is_login")
const avatar = Keychain.getData("avatar")

// Remove
Keychain.remove("token")

// Check existence
Keychain.contains("token")

// List all keys
Keychain.keys()

// Clear
Keychain.clear()

Pick a Keychain Value

const value = await Keychain.pick({
  title: "Choose Pairing Record",
  prompt: "Choose a pairing record from another script."
})
if (value !== null) {
  // Use the selected value. Do not log secrets.
}

pick(options?: { title?: string; prompt?: string }): Promise<string | null> opens a searchable, single-selection sheet. title sets its navigation title and prompt displays instructions above the list. Both are optional; blank text uses the default title or omits the prompt.

The user must explicitly enable local authentication if needed, and authenticate for every picker visit. The sheet identifies the calling script. Tap a key to select it, then tap Confirm at the top right to share its current value with the calling script. Confirm appears only after selection. Selecting a row alone does not read or return its value. Changing the search clears the selection.

The picker includes script, skill, workspace and execution scopes, including old script scopes. Shared storage and internal application credentials are excluded. It does not edit, delete, preview or copy values.

Closing or cancelling the sheet, cancelling authentication, or moving the app to the background returns null. An empty string remains "". Values must decode as UTF-8, matching Keychain.get(). Read failures and non-UTF-8 data are shown in the sheet so the user can retry, choose another key or cancel.

This API requires a foreground app with an available presentation window. Unsupported contexts (such as widgets or background execution), invalid argument types and concurrent picker requests reject with an error. Script termination dismisses the picker and releases the request; a terminated JavaScript context cannot receive a result. Each selection is a one-time disclosure and does not grant ongoing access to another script's keys.